diff options
Diffstat (limited to 'app/[lng]/test/table-v2/actions.ts')
| -rw-r--r-- | app/[lng]/test/table-v2/actions.ts | 326 |
1 files changed, 326 insertions, 0 deletions
diff --git a/app/[lng]/test/table-v2/actions.ts b/app/[lng]/test/table-v2/actions.ts new file mode 100644 index 00000000..f5fd5f66 --- /dev/null +++ b/app/[lng]/test/table-v2/actions.ts @@ -0,0 +1,326 @@ +"use server"; + +import db from "@/db/db"; +import { testProducts, testOrders, testCustomers } from "@/db/schema/test-table-v2"; +import { createTableService } from "@/components/client-table-v2/adapter/create-table-service"; +import { DrizzleTableState } from "@/components/client-table-v2/adapter/drizzle-table-adapter"; +import { productColumnDefs, OrderWithDetails, ServerColumnMeta } from "./column-defs"; +import { SQL, count, eq, desc, sql, asc } from "drizzle-orm"; +import { TestProduct } from "@/db/schema/test-table-v2"; + +// ============================================================ +// Pattern 1: Client-Side - 전체 데이터 로드 +// ============================================================ + +export async function getAllProducts() { + return await db.select().from(testProducts).orderBy(testProducts.id); +} + +// ============================================================ +// Pattern 2: Factory Service - 자동 생성된 서버 액션 +// ============================================================ + +// Server-side용 컬럼 정의 사용 (React 컴포넌트 없음) +export const getProductTableData = createTableService({ + db, + schema: testProducts, + columns: productColumnDefs, +}); + +// ============================================================ +// Pattern 2-B: Factory Service with Grouping Support +// ============================================================ + +/** + * 그룹 정보 타입 + */ +export interface GroupInfo { + groupKey: string; + groupValue: string | number | boolean | null; + count: number; + // 확장 시 로드된 하위 행들 + rows?: TestProduct[]; +} + +/** + * 그룹핑 응답 타입 + */ +export interface GroupedResponse { + groups: GroupInfo[]; + totalGroups: number; +} + +/** + * 일반 응답 타입 + */ +export interface NormalResponse { + data: TestProduct[]; + totalRows: number; + pageCount: number; +} + +/** + * 서버 사이드 그룹핑을 지원하는 상품 테이블 데이터 조회 + * + * @param tableState - 테이블 상태 (pagination, sorting, filters, grouping) + * @param expandedGroups - 확장된 그룹 키 목록 (예: ["category:Electronics", "status:active"]) + */ +export async function getProductTableDataWithGrouping( + tableState: DrizzleTableState, + expandedGroups: string[] = [] +): Promise<GroupedResponse | NormalResponse> { + const { grouping, pagination } = tableState; + + // 그룹핑이 없으면 일반 조회 + if (!grouping || grouping.length === 0) { + const result = await getProductTableData(tableState); + return result as NormalResponse; + } + + // 첫 번째 그룹핑 컬럼만 처리 (다중 그룹핑은 복잡도가 높음) + const groupColumnId = grouping[0]; + + // 서버 그룹핑 가능 여부 확인 + const columnDef = productColumnDefs.find( + col => 'accessorKey' in col && col.accessorKey === groupColumnId + ); + const meta = columnDef?.meta as ServerColumnMeta | undefined; + + if (!meta?.serverGroupable) { + // 서버 그룹핑 불가 - 전체 데이터 반환하여 클라이언트에서 처리 + console.warn(`Column "${groupColumnId}" does not support server grouping. Falling back to client-side.`); + const allData = await db.select().from(testProducts); + return { + data: allData, + totalRows: allData.length, + pageCount: 1, + }; + } + + // 그룹별 카운트 조회 + const groupColumn = getProductColumn(groupColumnId); + if (!groupColumn) { + throw new Error(`Unknown column: ${groupColumnId}`); + } + + const groupsResult = await db + .select({ + groupValue: groupColumn, + count: count(), + }) + .from(testProducts) + .groupBy(groupColumn) + .orderBy(asc(groupColumn)); + + // 그룹 정보 구성 + const groups: GroupInfo[] = await Promise.all( + groupsResult.map(async (g) => { + const groupKey = `${groupColumnId}:${g.groupValue}`; + const isExpanded = expandedGroups.includes(groupKey); + + let rows: TestProduct[] | undefined; + + // 확장된 그룹의 하위 행 로드 + if (isExpanded) { + rows = await db + .select() + .from(testProducts) + .where(eq(groupColumn, g.groupValue)) + .orderBy(testProducts.id) + .limit(pagination?.pageSize ?? 100); // 그룹 내 행 제한 + } + + return { + groupKey, + groupValue: g.groupValue, + count: Number(g.count), + rows, + }; + }) + ); + + return { + groups, + totalGroups: groups.length, + }; +} + +/** + * 컬럼 ID로 Drizzle 컬럼 객체 반환 + */ +function getProductColumn(columnId: string) { + const columnMap: Record<string, any> = { + id: testProducts.id, + sku: testProducts.sku, + name: testProducts.name, + category: testProducts.category, + price: testProducts.price, + stock: testProducts.stock, + status: testProducts.status, + isNew: testProducts.isNew, + createdAt: testProducts.createdAt, + updatedAt: testProducts.updatedAt, + }; + return columnMap[columnId]; +} + +// ============================================================ +// Pattern 3: Custom Service - 복잡한 조인 쿼리 +// ============================================================ + +export async function getOrderTableData(tableState: DrizzleTableState): Promise<{ + data: OrderWithDetails[]; + totalRows: number; + pageCount: number; +}> { + // Pattern 3에서는 DrizzleTableAdapter를 사용하지 않습니다. + // 조인된 결과의 컬럼들은 단일 테이블에 매핑되지 않기 때문입니다. + // 대신, 페이지네이션 값만 직접 계산합니다. + + const pageSize = tableState.pagination?.pageSize ?? 10; + const pageIndex = tableState.pagination?.pageIndex ?? 0; + const limit = pageSize; + const offset = pageIndex * pageSize; + + // Build ORDER BY clause based on sorting state + const orderByClauses = + tableState.sorting?.reduce<SQL<unknown>[]>((clauses, sort) => { + const columnMap: Record<string, any> = { + id: testOrders.id, + orderNumber: testOrders.orderNumber, + quantity: testOrders.quantity, + unitPrice: testOrders.unitPrice, + totalAmount: testOrders.totalAmount, + status: testOrders.status, + orderedAt: testOrders.orderedAt, + customerName: testCustomers.name, + customerEmail: testCustomers.email, + customerTier: testCustomers.tier, + productName: testProducts.name, + productSku: testProducts.sku, + }; + + const column = columnMap[sort.id]; + if (!column) return clauses; + + clauses.push(sort.desc ? desc(column) : asc(column)); + return clauses; + }, []) ?? []; + + // 커스텀 조인 쿼리 작성 + const data = await db + .select({ + id: testOrders.id, + orderNumber: testOrders.orderNumber, + quantity: testOrders.quantity, + unitPrice: testOrders.unitPrice, + totalAmount: testOrders.totalAmount, + status: testOrders.status, + orderedAt: testOrders.orderedAt, + // 고객 정보 조인 + customerName: testCustomers.name, + customerEmail: testCustomers.email, + customerTier: testCustomers.tier, + // 상품 정보 조인 + productName: testProducts.name, + productSku: testProducts.sku, + }) + .from(testOrders) + .leftJoin(testCustomers, eq(testOrders.customerId, testCustomers.id)) + .leftJoin(testProducts, eq(testOrders.productId, testProducts.id)) + .orderBy(...(orderByClauses.length > 0 ? orderByClauses : [desc(testOrders.orderedAt)])) + .limit(limit) + .offset(offset); + + // 총 개수 쿼리 + const totalResult = await db + .select({ count: count() }) + .from(testOrders); + + const totalRows = Number(totalResult[0]?.count ?? 0); + + return { + data: data as OrderWithDetails[], + totalRows, + pageCount: Math.ceil(totalRows / pageSize), + }; +} + +// ============================================================ +// Pattern 3-B: Custom Service with Grouping (Orders by Status) +// ============================================================ + +export interface OrderGroupInfo { + groupKey: string; + groupValue: string; + count: number; + totalAmount: number; + rows?: OrderWithDetails[]; +} + +/** + * 주문 데이터를 상태별로 그룹핑하여 조회 + */ +export async function getOrderTableDataGroupedByStatus( + expandedGroups: string[] = [] +): Promise<{ groups: OrderGroupInfo[]; totalGroups: number }> { + // 상태별 그룹 집계 + const groupsResult = await db + .select({ + status: testOrders.status, + count: count(), + totalAmount: sql<number>`SUM(${testOrders.totalAmount}::numeric)`, + }) + .from(testOrders) + .groupBy(testOrders.status) + .orderBy(testOrders.status); + + const groups: OrderGroupInfo[] = await Promise.all( + groupsResult.map(async (g) => { + const groupKey = `status:${g.status}`; + const isExpanded = expandedGroups.includes(groupKey); + + let rows: OrderWithDetails[] | undefined; + + if (isExpanded) { + // 확장된 그룹의 상세 주문 조회 (조인 포함) + const orderRows = await db + .select({ + id: testOrders.id, + orderNumber: testOrders.orderNumber, + quantity: testOrders.quantity, + unitPrice: testOrders.unitPrice, + totalAmount: testOrders.totalAmount, + status: testOrders.status, + orderedAt: testOrders.orderedAt, + customerName: testCustomers.name, + customerEmail: testCustomers.email, + customerTier: testCustomers.tier, + productName: testProducts.name, + productSku: testProducts.sku, + }) + .from(testOrders) + .leftJoin(testCustomers, eq(testOrders.customerId, testCustomers.id)) + .leftJoin(testProducts, eq(testOrders.productId, testProducts.id)) + .where(eq(testOrders.status, g.status)) + .orderBy(desc(testOrders.orderedAt)) + .limit(50); + + rows = orderRows as OrderWithDetails[]; + } + + return { + groupKey, + groupValue: g.status, + count: Number(g.count), + totalAmount: Number(g.totalAmount) || 0, + rows, + }; + }) + ); + + return { + groups, + totalGroups: groups.length, + }; +} |
